docs(rfc): 插件扩展面盘点与边界(RFC 0011) - #385
Draft
NWYLZW wants to merge 9 commits into
Draft
Conversation
Establish a referenceable baseline for what the plugin system can actually do today, compare it structurally against DeepSeek Harness / Cordis, and record the boundary decisions that keep getting re-litigated. - current-surface: inventory of the existing extension surface with source locations (config-layer plugin graph, extension points + plugin APIs, toolUsePresentations, the @oneworks/hooks middleware chain, server runtime primitives, security boundaries), plus a record of three misjudgements made during the survey - dsh-comparison: structural comparison pinned to fixed upstream revisions, covering interception vs registration seams, external code-agent scheduling, and the generated-catalog documentation model - boundaries: seven referenceable disciplines (plugins cannot create plugins, view extension ordering, registration seams belong on the resident runtime, no accepted-then-ignored, trust/scope semantics, model-visible implies logged, the three-role seam definition) - actions: prioritised items split by whether they need a product decision Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Experience Review
Before merge, make sure the PR body contains this completed checklist:
## Experience Review
- [x] 已判断是否需要沉淀经验
- [x] 如需要,已运行 `$post-task-experience-review`
- [x] reviewer `PASS` / `NOT APPLICABLE` 后才进入 mergeChecklist to confirm:
- Decide whether this PR produced reusable project experience.
- If experience should be captured, run
$post-task-experience-review. - Merge only after the independent reviewer reports
PASSorNOT APPLICABLEfor the Experience Review Result. - For workflow, permission, or release-rule documentation changes, also add this checked section to the PR body after an independent read-only conflict review:
## Policy Conflict Review
- [x] Independent read-only reviewer checked workflow, permission, and release-rule conflicts and reported PASSCollapse @oneworks/hooks and the plugin runtime into one extension surface. The hook subprocess becomes a normalising reporter; plugin code moves into whichever process drives the task, consuming one internal event stream. - events-api: ctx.events with three modes narrowed from Cordis's five. emit/parallel/serial fold into `notify` (awaiting is the dispatcher's choice, not the event's); `waterfall` becomes `transform`; `bail` is replaced by `decide` — an order-independent, monotonically-tightening adjudication that encodes "capabilities add, permissions subtract" into dispatch semantics rather than leaving it to each event's implementation - events: the vocabulary, renamed to DSH's namespace/kebab convention for migration parity, with per-source availability grading so unsupported subscriptions fail loud; four gap points identified against DSH, all in the model-request and around-dispatch layers - runtime: reporter contract, endpoint resolution (no daemon needed — the process driving the task is alive by construction), permission layering where host baseline is synchronous and plugins can only tighten, and an explicit priority contract replacing the current array-order guarantee - migration: five reversible steps, compat shim mapping for the old <pkg>/hooks entry, and an honest capability matrix for a DSH plugin shim Also corrects the hook event count in RFC 0011 from 15 to 14. Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
Revise RFC 0012's event API: instead of narrowing Cordis's five dispatch modes down to three, keep all of them under their original names and move the restrictions onto the event definition, where they can be checked mechanically at define time. Three earlier exclusions were wrong: - `emit` was dropped by over-generalising "hook events cross a process boundary" to "all events do". Plugin-to-plugin events live in one runtime, where synchronous dispatch is both valid and preferable. - `parallel` and `serial` were folded together on the premise that awaiting is the dispatcher's choice. That conflated two things: the real distinction is whether listeners can observe each other's side effects, which is a property of the event, not the call site. - `bail` was banned for a real hazard applied too broadly. First-responder resolution is legitimate; only permission adjudication is unsafe, and that already has `decide`. Constraints now: `emit` cannot be cross-process, and `security: true` events accept only `decide`. Keeping Cordis's names also restores full mode parity with DSH apart from `tools/pre-execute`, which is `security: true` and therefore `decide` on our side. Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The constraint stands but the stated reason was wrong. "Cross-process cannot dispatch synchronously" is inaccurate: Atomics.wait() over a SharedArrayBuffer with receiveMessageOnPort() gives real synchronous blocking between worker_threads, and Node permits Atomics.wait on the main thread. Only true separate processes are impractical, since a SharedArrayBuffer cannot cross an IPC pipe. The actual reason is semantic redundancy. What `emit` buys — no microtask scheduling, listener on the dispatcher's stack so exceptions propagate directly, deterministic uninterleaved order — is entirely lost once a process boundary is crossed: the IPC round trip dwarfs scheduling cost, the stack is already severed and errors must be serialised, and ordering comes from serial transport rather than synchronous semantics. A blocking notification is precisely `serial`, which already exists. This rationale is transport-independent, so it survives a future move of the hook worker from child_process to worker_threads. Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The plugin system had no entry under .oo/rules/ — its normative content was scattered across three RFC series, a hand-written usage doc, and the code. Establish .oo/rules/PLUGIN-SYSTEM.md plus a plugin-system/ directory following the same shape as HOOKS.md, so rules carry the specification and RFCs stay as the record of why each decision was made. This commit lands the entry, the overview, and the communication chapter. The overview doubles as a gap map: every capability is tagged implemented, in-design, or missing, so nobody writes plugins against a design. Four inconsistencies surfaced while writing it, all recorded in the overview: - executeCommand accepts a scoped <scope>/<id> and will HTTP to another plugin's server command, while the usage doc describes only the same-scope path - client can reach across plugins but server cannot: invokeChannel binds the scope host-side, so a server plugin only ever calls its own channel - RFC 0012's three-level availability grading is coarser than reality — hooks/events.md already carries a per-adapter, per-event matrix with a canBlock dimension, and the event spec should defer to it - same-scope command id collisions remain unverified Remaining chapters are listed as pending rather than linked, so the index has no dead links. Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The spec was accumulating chapters without a spine — twelve documents across two RFC series and a rules directory, each capability argued separately, no single sentence saying what a plugin is. Give it one. A plugin stands in exactly three relations to the host and to other plugins, distinguished by who owns the result: - contribute — hands over data, the receiver decides how to use it - participate — gets called inside someone else's flow, the host merges - provide — exposes a named capability, the plugin itself decides The three are mutually exclusive and complete, so a new capability must classify into one; if it doesn't, the model is wrong and changes first. Crossed with where code runs (client / server / reporter), this yields a matrix where the blanks are the gaps, and it reframes the open work in one line each: - contribute is healthy on both ends - participate sits in the wrong place: it belongs on the resident server but currently runs in the reporter subprocess with a logger-only ctx, which is exactly what RFC 0012 moves - provide is crippled server-side, since channels and registerApi bind scope host-side and a server plugin can only ever call its own Chapters now follow the model: contribute.md, participate.md, provide.md replace the earlier communication.md, whose content spanned two relations and so obscured the distinction. Docs only; no runtime behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The mode constraint table only covered participate-type channels, but transport reachability and security grading are properties of every communication channel, not just events. - README: define `transport` and a three-tier `security` grading, plus where the security property attaches per relation (channel for contribute/participate, ctx capability surface for provide) - contribute: asset directories are `advisory`, not `none` — skills and rules reach the system prompt, mcp reaches the toolset - provide: provide-type channels carry no `security` field; the handler is the plugin's own code, so the real bound is what ctx exposes - participate: frame the mode table as a specialization of the general rule rather than a standalone invention Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
README and provide.md both assert that the real bound on a plugin is the ctx capability surface, with nothing behind the claim. This writes that chapter. The starting point is that there is no sandbox: client, server, and the hook reporter all load plugin code through a plain dynamic import into the host's own module graph. So "plugins cannot X" throughout the spec means the host offers no channel for X, not that the host prevents it. Documents the four boundaries that are real — the first-party capability gate in createServerContext, the source-group whitelist that gate rests on, the realpath-based scoped path check, and the loopback-only proxy with credential header stripping — plus the build-time source boundary and diagnostic redaction. Adds the review checklist for new ctx capabilities, and an explicit list of guarantees we do not make. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
RFC 0011 surveyed the extension surface, 0012 converged hooks, and the rules spec wrote both down. All three describe; none of them made any code smaller. plugin-registry.ts still carries nine near-identical scope filters, two separate pending queues, and contract checking in exactly one place. The kernel is one law: every named collection has a single end and a many end, and the definer always holds the single end. Registry gives the definer read and everyone contribute; event gives the definer emit and everyone on; api gives the definer provide and everyone invoke. The three relations stop being a taxonomy we declared and become a consequence of who defined the collection. Ownership, pending/drain, and contract checking move onto the collection itself, so a new channel cannot forget them. `onAvailable` and the `command` type both disappear — the first because waiting is now a kernel property, the second because it is an api missing exactly the four things collections provide by default. Two edges stay deliberately outside: ctx construction and participant instantiation. Admitting either is Cordis, and costs the guarantee that the host baseline is the floor. Includes the full interface surface and a six-phase migration where phases 1-4 leave every existing plugin contract untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #384
Summary
rfc.md的分章惯例拆五篇,为"插件到底能做什么"建立可引用的单一事实源0011-plugin-extensibility.md—— 总览、结论摘要、调研方法0011-plugin-extensibility-current-surface.md—— 现有扩展面盘点,逐条标注源码位置;含调研中三次误判的记录0011-plugin-extensibility-dsh-comparison.md—— 与 DeepSeek Harness / Cordis 的结构对照,钉在固定上游 revision0011-plugin-extensibility-boundaries.md—— 七条可引用的设计纪律0011-plugin-extensibility-actions.md—— 行动项,按是否需要产品决策分组rfc.md增加"插件扩展面 RFC"索引段纯文档,不改任何运行时行为。
为什么需要这份 RFC
调研过程中对自身扩展面出现过三次错误判断,而调研是拿着完整代码库做的:
children是组合依赖;extensionPoints.onAvailable+pluginApis.call是完整的运行时依赖装配,含等待语义、自动回收、epoch 竞态保护toolUsePresentations是完整的声明式渲染扩展,已在 cua-driver / browser-driver / external-browser-driver 生产使用@oneworks/hooks有 15 个事件,含PreToolUse否决权、GenerateSystemPrompt改写权、continue: false停机权根因是能力面分散在手写文档(
ui-runtime.md400+ 行)、SKILL.md 与源码之间。误判记录刻意保留在 RFC 里,作为 P0-2「生成式能力目录」的论据。七条纪律(供评审重点讨论)
Changelog
Screenshots
Not applicable
Validation
pnpm dprint fmt通过(格式化了 3 个文件,已随提交)rfc.md中全部相对链接可解析,无 broken linkdeepseek-ai/deepseek-harness@99f6f02与cordiverse/cordis@f46ae95,升级上游后需重新核对Review Scope
评审建议聚焦两点:
行动项部分 P2 两项(model provider seam、适配器 seam 化)标注为"需要产品决策",本 PR 不推进,仅记录论据。
Experience Review
$post-task-experience-reviewPASS/NOT APPLICABLE后才进入 mergePolicy Conflict Review
Generated by Claude Code